-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Don't re-authorize objects that were part of a scoped list #3994
Conversation
cc @hvenables I've worked up a basic implementation, described here: https://github.com/rmosolgo/graphql-ruby/pull/3994/files#diff-d60a70644c1e9b36288190a468466b7186cd98ab45bb3519e30b922fc9708cac Does that sound like it would work for what you had in mind? |
Would love to have this available in next version. We have usecase for this as well. Let me know if I can help in anyway. |
Sure thing, I'll try to wrap it up soon. The trick is determining when an application-defined Instead, I think I'll try inspecting |
@hvenables left the company (enjoy Shopify Harry 😛) so I'll have to fill in on feedback! This definitely works for us. Our GraphQL Pundit authorization architecture is essentially
So given your API we would probably implement We're still very keen for this API btw! Authorization induced N+1s is one of our biggest perf issues in our GraphQL API. |
Thanks for sharing your thoughts about it! I'll make some time to land this PR soon. |
I've been thinking about this more lately, in the context of #4087, because we perform the same scoping that GraphQL Pro Pundit does to ActiveRecord::Relations, within all our ActiveRecord::Relation sources. In this context, I cannot think of a way that you could indicate to the runtime "hey, I know you're returning an instance of MyRecord, but trust me, I loaded that through a dataloader which scoped it, so you do not need to authorize it again". The only option I can think that could work is
e.g. theoretical API scope = MyRecord.where(visible: true).mark_authorized!(:view) # sets @authorized = :view
results = scope.to_a # ActiveRecord sets @authorized on each instantiated record
results.first.pre_authorized?(:view) => true # accesses record specific @authorized I have no idea if this is actually possible without monkeypatching ActiveRecord yet, but I thought I'd put my thoughts somewhere others with more experience might see! EDIT: Yeah, I looked into the AR source to see how 7.0's strict record loading works and it certainly suggests there's no API flexible enough to do this from the outside. However, I just realised that you don't need to do pass the state through the relation. The Dataloader is responsible for instantiating the relation, so it is well placed to add this state to the records! We could easily modify a source's |
We appear to be running into the same issue, any chance this will make it into a release soon? Thanks! |
I'll address the lint failure in another branch... |
Thanks for merging this. Any chance we can get an ETA on when it would potentially be released? |
I just finally released this in GraphQL-Ruby 2.1.0 😅 |
Thanks, we will try it out and report back! |
Hi! Thanks for the contributions. Two questions:
I could totally be misunderstanding this -- after years of working with these parts, I still haven't wrapped my head around it. Please fill me in if I'm missing anything important, I'd love to be wrong on this. |
@@ -10,7 +10,13 @@ def after_resolve(object:, arguments:, context:, value:, memo:) | |||
else | |||
ret_type = @field.type.unwrap | |||
if ret_type.respond_to?(:scope_items) | |||
ret_type.scope_items(value, context) | |||
scoped_items = ret_type.scope_items(value, context) | |||
if !scoped_items.equal?(value) && !ret_type.reauthorize_scoped_objects |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per my comment, perhaps this should also check the type of value, so that it doesn't skip authorizing arrays twice?
Hey @bmulholland, thanks for pointing out that issue with the Changelog. I updated it in a3c093f.
Could you give a before-and-after example of a refactor where this would happen? I would expect lists to go through |
Thanks for clarifying, appreciate it!
Thanks for getting me to check this again. It looks like this is only in effect when using the Pundit integration -- so the footgun is limited to that scenario. It looks like this is actually a risk for us, but only from our own app's code. That's because of GraphQL-Ruby-related history: We were trying to use the Pundit integration but honestly we could never figure out how the concepts fit together so we dropped the attempt. However, because of that, we still have explicit handling of arrays in our scope_items calls, which leads to this issue. We'll discuss the best way to handle this internally: either somehow ensure that the "disable authorized?" option is never enabled, or have a policy that Arrays in scope_items are always scoped too. No action needed from GraphQL, though this does feel a bit too close for comfort. |
The documentation for this change would lead me to believe that the default is to reauthorize scoped objects, however after observing the behavior and examining the code it would seem that the default behavior has changed (a breaking change) and that scoped objects are no longer re-authorized. This means that scopes which were written too broadly with the assumption that objects would be further authorized by the policy method will now unintentionally be exposing objects after upgrading. The check seems to be here, and I don't see anything defaulting this value to true:
Additionally, unless I've got something setup incorrectly, I have found that calling |
Uh, what? That's really alarming: as noted above, this is a big risk for us. It's incredibly critical that we don't expose data to the wrong users, and we need all the checks that we can get. I think that's worth it's own Issue, right? More broadly, as I've noted a few times, there's several moving parts here that lead to a high risk for disastrous integration-level bugs: Dataloaders, array handling, and scope re-authorization could easily combine together to skip all checks, exposing all data to users. Since there are several options and configuration variations of these, this is probably hard to both test thoroughly and reason about. Even if the specifics here mean there's no/low concern, I've already had a few similar conversations on this project that have ended with that same outcome. What happens when several areas of "no concern" collide? |
I agree. Also, in my situation, while I would like to get as close as possible to loading only the permitted objects for performance reasons (ie. I'm not going to load objects for a completely different tenant), sometimes there is code that runs in the policy method that is difficult or impossible to reproduce in a database scope. So certainly I don't think the default behavior should have changed between versions. |
@rmosolgo Awesome, thank you very much for the fast resolution! |
I share this concern of yours @bmulholland. We are GraphQL Pro (and Ent) users and make heavy use of Dataloaders and the GraphQL Pro Pundit integration. I tried to adopt this new optimisation in a spike, but ran into a problem like yours: it has no effect for Dataloaders. I believe because they (or at least ours) load an Array, and Pundit can't do anything useful with scoping an already instantiated Array. That's not a security issue at least, but it does mean these two GraphQL Ruby provided tools are not compatible with one another. I've been doing a lot of GraphQL Ruby perf optimisation work lately on our codebase, and I keep coming back to one thing: reasoning about this stuff is getting incredibly difficult. I have used GraphQL Ruby for years and still I have a hard time reasoning about how these factors combine: (I know the answers to these Qs now, or so I hope, but just to illustrate how difficult this is to reason about):
We've used Module#prepend to wrap the GraphQL Pro Pundit integration with verbose debug logging of the decisions it makes, and still I regularly I'm coming around to the view point that
Is it constructive to continue this conversation, and if so is GH Discussions the best place to do so? I appreciate my problems can largely be solved by ignoring this features existence and building our own Pundit integration, but since it seems others in the community have similar concerns, perhaps its worth discussing nonetheless? PS I don't want to come across as ungrateful, GraphQL Ruby is a wonderful piece of software ❤️ |
Hey @bessey and @bmulholland, thanks for your detailed writeups on the conflict between these features -- and sorry for the trouble you've run into so far on them :S I can see that they need some reconsideration. Over the next couple of days I'm going to re-read your descriptions and suggestions and open a new issue with some proposals. I'll follow up back here when I do 👍 |
I went digging on the Pundit Arrays issue and found a previous suggestion which seems like a good candidate for a new default behavior. I worked out the code locally and wrote up a description here: #4726 @bmulholland and @bessey , could you let me know on that issue what you think, if you have a minute? Thanks! |
👋 Hey @rmosolgo. We are updating our GraphQL gem to 2.3 and want to migrate to using the In order to do this we want to use
It’s not clear to me if we are missusing this feature. Why do Did I understand correctly the behavior? Is it possible to introduce any bugs or potential security issues by returning a clone of |
Hey @iulia-b, great question. Yes, that's the best way to handle your situation. For lack of better implementation, GraphQL-Ruby checks In your case, you're using the resolver to scope the list, so The security issue would be if any other fields return lists of this same type. Do those other lists need scoping? in that case, you'd need a way of detecting whether I hope this helps! |
Thank you for the explanations, @rmosolgo! I'm curious if there might be a more efficient solution instead of cloning. Could we enhance the return type of the |
I'm open to adding some kind of flag method on the returned object, something like |
I create this issue to track it: |
After a list has been scoped, don't re-run authorization on the objects it permitted.
Fixes #3893
TODO:
connection -> edges -> node
andconnection -> nodes
.equal?
to test same-object, add a test for that behavior